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