packages/ztd-query-pdo-adapter/tests/Unit/Driver/PdoStatementTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Driver;
6
7use PDO;
8use PDOException;
9use PHPUnit\Framework\Attributes\CoversClass;
10use PHPUnit\Framework\TestCase;
11use ZtdQuery\Adapter\Pdo\Driver\PdoStatement;
12use ZtdQuery\Connection\StatementInterface;
13use ZtdQuery\Schema\ColumnTypeFamily;
14
15#[CoversClass(PdoStatement::class)]
16#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Adapter\Pdo\ZtdPdoException::class)]
17#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Adapter\Pdo\ZtdPdoStatement::class)]
18#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Adapter\Pdo\ZtdPdo::class)]
19#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Adapter\Pdo\Driver\PdoConnection::class)]
20#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Adapter\Pdo\Session\StatementExecution::class)]
21#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Adapter\Pdo\Session\Bindings::class)]
22#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Adapter\Pdo\Session\BufferedRow::class)]
23#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Adapter\Pdo\Session\DriverSessionFactory::class)]
24#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Adapter\Pdo\Session\ParameterKind::class)]
25#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Adapter\Pdo\Session\ParameterBinder::class)]
26#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Adapter\Pdo\Session\PreparedQuery::class)]
27#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Adapter\Pdo\Session\ConnectionExecution::class)]
28final class PdoStatementTest extends TestCase
29{
30 public function testImplementsStatementInterface(): void
31 {
32 $pdo = new PDO('sqlite::memory:');
33 $pdo->exec('CREATE TABLE t (id INTEGER)');
34 $nativeStmt = $pdo->query('SELECT * FROM t');
35 self::assertNotFalse($nativeStmt);
36
37 $stmt = new PdoStatement($nativeStmt);
38
39 self::assertContains(StatementInterface::class, class_implements($stmt));
40 }
41
42 public function testFetchAllReturnsAssociativeArrays(): void
43 {
44 $pdo = new PDO('sqlite::memory:');
45 $pdo->exec('CREATE TABLE t (id INTEGER, name TEXT)');
46 $pdo->exec("INSERT INTO t VALUES (1, 'a')");
47 $pdo->exec("INSERT INTO t VALUES (2, 'b')");
48
49 $nativeStmt = $pdo->query('SELECT * FROM t ORDER BY id');
50 self::assertNotFalse($nativeStmt);
51
52 $stmt = new PdoStatement($nativeStmt);
53 $rows = $stmt->fetchAll();
54
55 self::assertCount(2, $rows);
56 self::assertSame(1, $rows[0]['id']);
57 self::assertSame('a', $rows[0]['name']);
58 }
59
60 public function testRowCountReturnsAffectedRows(): void
61 {
62 $pdo = new PDO('sqlite::memory:');
63 $pdo->exec('CREATE TABLE t (id INTEGER)');
64 $pdo->exec('INSERT INTO t VALUES (1)');
65 $pdo->exec('INSERT INTO t VALUES (2)');
66
67 $nativeStmt = $pdo->prepare('DELETE FROM t');
68 self::assertNotFalse($nativeStmt);
69 $nativeStmt->execute();
70
71 $stmt = new PdoStatement($nativeStmt);
72
73 self::assertSame(2, $stmt->rowCount());
74 }
75
76 public function testExecuteReturnsTrueOnSuccess(): void
77 {
78 $pdo = new PDO('sqlite::memory:');
79 $pdo->exec('CREATE TABLE t (id INTEGER)');
80
81 $nativeStmt = $pdo->prepare('INSERT INTO t VALUES (1)');
82 self::assertNotFalse($nativeStmt);
83
84 $stmt = new PdoStatement($nativeStmt);
85
86 self::assertTrue($stmt->execute());
87 }
88
89 public function testResultColumnsDelegateTypesForEmptyResult(): void
90 {
91 $pdo = new PDO('sqlite::memory:');
92 $pdo->exec('CREATE TABLE t (id INTEGER, name TEXT, score REAL)');
93 $nativeStmt = $pdo->query('SELECT * FROM t WHERE 1 = 0');
94 self::assertNotFalse($nativeStmt);
95
96 $resolver = new \ZtdQuery\Platform\Sqlite\SqlitePdoResultColumnTypeResolver();
97 $columns = (new PdoStatement($nativeStmt))->resultColumns($resolver);
98
99 self::assertCount(3, $columns);
100
101 self::assertSame(['id', 'name', 'score'], array_map(static fn ($column) => $column->name, $columns));
102 self::assertSame(ColumnTypeFamily::INTEGER, $columns[0]->type->family);
103 self::assertSame(ColumnTypeFamily::TEXT, $columns[1]->type->family);
104 self::assertSame(ColumnTypeFamily::FLOAT, $columns[2]->type->family);
105 }
106
107 public function testExecutePreservesTheNativeFailureDetails(): void
108 {
109 $pdo = new PDO('sqlite::memory:');
110 $pdo->exec('CREATE TABLE users (id INTEGER PRIMARY KEY)');
111 $pdo->exec('INSERT INTO users VALUES (1)');
112 $native = $pdo->prepare('INSERT INTO users VALUES (1)');
113 self::assertNotFalse($native);
114 try {
115 (new PdoStatement($native))->execute();
116 self::fail('A duplicate primary key must fail.');
117 } catch (\ZtdQuery\Connection\Exception\DatabaseException $exception) {
118 self::assertSame(23000, $exception->getCode());
119 self::assertSame(19, $exception->getDriverErrorCode());
120 self::assertInstanceOf(PDOException::class, $exception->getPrevious());
121 }
122 }
123 public function testFetchAllPreservesNativeColumnCaseAndDuplicateLabels(): void
124 {
125 $pdo = new PDO('sqlite::memory:', options: [PDO::ATTR_CASE => PDO::CASE_LOWER]);
126 $statement = $pdo->query('SELECT 7 AS MixedName, NULL AS OptionalValue, 9 AS MixedName');
127 self::assertNotFalse($statement);
128 self::assertSame([['mixedname' => 9, 'optionalvalue' => null]], (new PdoStatement($statement))->fetchAll());
129 }
130
131
132 public function testFetchAllPreservesNumericColumnLabelsAndValueTypes(): void
133 {
134 $native = new PDO('sqlite::memory:');
135 $statement = $native->query('SELECT 7 AS "12", 1.5 AS amount, NULL AS optional');
136 self::assertNotFalse($statement);
137 self::assertSame([[12 => 7, 'amount' => 1.5, 'optional' => null]], (new PdoStatement($statement))->fetchAll());
138 }
139}
140