packages/ztd-query-pdo-adapter/tests/Unit/ZtdPdoTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit;
6
7use PDO;
8use PHPUnit\Framework\Attributes\CoversClass;
9use PHPUnit\Framework\TestCase;
10use ZtdQuery\Adapter\Pdo\Driver\PdoConnection;
11use ZtdQuery\Adapter\Pdo\Driver\PdoStatement;
12use ZtdQuery\Adapter\Pdo\ZtdPdo;
13use ZtdQuery\Adapter\Pdo\ZtdPdoException;
14use ZtdQuery\Adapter\Pdo\ZtdPdoStatement;
15
16#[CoversClass(ZtdPdo::class)]
17#[\PHPUnit\Framework\Attributes\UsesClass(ZtdPdoException::class)]
18#[\PHPUnit\Framework\Attributes\UsesClass(ZtdPdoStatement::class)]
19#[\PHPUnit\Framework\Attributes\UsesClass(PdoConnection::class)]
20#[\PHPUnit\Framework\Attributes\UsesClass(PdoStatement::class)]
21#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Adapter\Pdo\Session\StatementExecution::class)]
22#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Adapter\Pdo\Session\Bindings::class)]
23#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Adapter\Pdo\Session\BufferedRow::class)]
24#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Adapter\Pdo\Session\DriverSessionFactory::class)]
25#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Adapter\Pdo\Session\ParameterKind::class)]
26#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Adapter\Pdo\Session\ParameterBinder::class)]
27#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Adapter\Pdo\Session\PreparedQuery::class)]
28#[\PHPUnit\Framework\Attributes\Medium]
29#[CoversClass(\ZtdQuery\Adapter\Pdo\Session\ConnectionExecution::class)]
30final class ZtdPdoTest extends TestCase
31{
32    public function testPublicApiAdaptsPdoWithoutAddingDriverSpecificOperations(): void
33    {
34        $pdoMethods = get_class_methods(PDO::class);
35        $lifecycleMethods = ['connect', 'fromPdo', 'enableZtd', 'disableZtd', 'isZtdEnabled'];
36
37        self::assertSame([], array_values(array_diff(
38            get_class_methods(ZtdPdo::class),
39            $pdoMethods,
40            $lifecycleMethods,
41        )));
42    }
43
44    public function testExplicitPlatformFactoryCreatesAnIsolatedSession(): void
45    {
46        $native = new PDO('sqlite::memory:');
47        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
48        $pdo = ZtdPdo::fromPdo($native, factory: new \ZtdQuery\Platform\Sqlite\SqliteSessionFactory());
49        self::assertSame(1, $pdo->exec("INSERT INTO users VALUES (1, 'Alice')"));
50        $result1 = $pdo->query('SELECT name FROM users');
51        self::assertNotFalse($result1);
52        self::assertSame('Alice', $result1->fetchColumn());
53        $result2 = $native->query('SELECT COUNT(*) FROM users');
54        self::assertNotFalse($result2);
55        self::assertSame(0, $result2->fetchColumn());
56        $pdo->disableZtd();
57        $pdo->enableZtd();
58        $result3 = $pdo->query('SELECT name FROM users');
59        self::assertNotFalse($result3);
60        self::assertSame('Alice', $result3->fetchColumn());
61    }
62
63    public function testExecBatchReturnsTheLastStatementCount(): void
64    {
65        $native = new PDO('sqlite::memory:');
66        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
67        $pdo = ZtdPdo::fromPdo($native);
68        self::assertSame(1, $pdo->exec("INSERT INTO users VALUES (1, 'Alice'), (2, 'Bob'); UPDATE users SET name = 'Carol' WHERE id = 2"));
69        $result4 = $pdo->query('SELECT * FROM users ORDER BY id');
70        self::assertNotFalse($result4);
71        self::assertSame([['id' => 1, 'name' => 'Alice'], ['id' => 2, 'name' => 'Carol']], $result4->fetchAll(PDO::FETCH_ASSOC));
72    }
73
74    public function testAutoDetectionForSqliteDriver(): void
75    {
76        (fn () => class_exists('ZtdQuery\\Platform\\Sqlite\\SqliteSessionFactory') || self::markTestSkipped('ztd-query-sqlite package is not installed.'))();
77
78        $pdo = new PDO('sqlite::memory:');
79        $ztdPdo = ZtdPdo::fromPdo($pdo);
80
81        self::assertTrue($ztdPdo->isZtdEnabled());
82    }
83
84    public function testEnableZtdPutsTheShadowBackInFrontOfTheDatabase(): void
85    {
86        $native = new PDO('sqlite::memory:');
87        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
88        $native->exec("INSERT INTO users (name) VALUES ('ada'), ('grace')");
89        $ztdPdo = ZtdPdo::fromPdo($native);
90        $ztdPdo->disableZtd();
91
92        $ztdPdo->enableZtd();
93
94        self::assertTrue($ztdPdo->isZtdEnabled());
95    }
96
97    public function testDisableZtdLetsStatementsReachTheDatabase(): void
98    {
99        $native = new PDO('sqlite::memory:');
100        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
101        $native->exec("INSERT INTO users (name) VALUES ('ada'), ('grace')");
102        $ztdPdo = ZtdPdo::fromPdo($native);
103
104        $ztdPdo->disableZtd();
105
106        self::assertFalse($ztdPdo->isZtdEnabled());
107    }
108
109    public function testIsZtdEnabledSaysWritesAreShadowedFromTheStart(): void
110    {
111        $native = new PDO('sqlite::memory:');
112        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
113        $native->exec("INSERT INTO users (name) VALUES ('ada'), ('grace')");
114        $ztdPdo = ZtdPdo::fromPdo($native);
115
116        self::assertTrue($ztdPdo->isZtdEnabled());
117    }
118
119    public function testPrepareAnswersAStatementThatShadowsWhatItIsRunWith(): void
120    {
121        $native = new PDO('sqlite::memory:');
122        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
123        $native->exec("INSERT INTO users (name) VALUES ('ada'), ('grace')");
124        $ztdPdo = ZtdPdo::fromPdo($native);
125
126        $statement = $ztdPdo->prepare('SELECT * FROM users');
127
128        self::assertNotFalse($statement);
129    }
130
131    public function testPrepareHandsTheStatementStraightToPdoWhileZtdIsOff(): void
132    {
133        $native = new PDO('sqlite::memory:');
134        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
135        $native->exec("INSERT INTO users (name) VALUES ('ada'), ('grace')");
136        $ztdPdo = ZtdPdo::fromPdo($native);
137        $ztdPdo->disableZtd();
138
139        $statement = $ztdPdo->prepare('SELECT * FROM users');
140
141        self::assertNotInstanceOf(ZtdPdoStatement::class, $statement);
142    }
143
144    public function testQueryReadsTheShadowRatherThanTheTable(): void
145    {
146        $native = new PDO('sqlite::memory:');
147        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
148        $native->exec("INSERT INTO users (name) VALUES ('ada'), ('grace')");
149        $ztdPdo = ZtdPdo::fromPdo($native);
150
151        $statement = $ztdPdo->query('SELECT * FROM users');
152
153        self::assertSame([], $statement === false ? [false] : $statement->fetchAll(PDO::FETCH_ASSOC));
154    }
155
156    public function testQueryReadsBackWhatWasWrittenThroughZtd(): void
157    {
158        $native = new PDO('sqlite::memory:');
159        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
160        $native->exec("INSERT INTO users (name) VALUES ('ada'), ('grace')");
161        $ztdPdo = ZtdPdo::fromPdo($native);
162        $ztdPdo->exec("INSERT INTO users (id, name) VALUES (3, 'linus')");
163
164        $statement = $ztdPdo->query('SELECT * FROM users');
165
166        self::assertSame([['id' => 3, 'name' => 'linus']], $statement === false ? [] : $statement->fetchAll(PDO::FETCH_ASSOC));
167    }
168
169    public function testQueryReadsInTheFetchModeItIsGiven(): void
170    {
171        $native = new PDO('sqlite::memory:');
172        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
173        $native->exec("INSERT INTO users (name) VALUES ('ada'), ('grace')");
174        $ztdPdo = ZtdPdo::fromPdo($native);
175        $ztdPdo->exec("INSERT INTO users (id, name) VALUES (3, 'linus')");
176
177        $statement = $ztdPdo->query('SELECT * FROM users', PDO::FETCH_NUM);
178
179        self::assertSame([[3, 'linus']], $statement === false ? [] : $statement->fetchAll());
180    }
181
182    public function testConnectOpensAConnectionWithZtdAlreadyInFrontOfIt(): void
183    {
184        self::assertSame(ZtdPdo::class, ZtdPdo::connect('sqlite::memory:')::class);
185    }
186
187    public function testBeginTransactionOpensOneOnTheShadowAsWellAsTheDatabase(): void
188    {
189        $native = new PDO('sqlite::memory:');
190        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
191        $native->exec("INSERT INTO users (name) VALUES ('ada'), ('grace')");
192        $ztdPdo = ZtdPdo::fromPdo($native);
193
194        self::assertSame([true, true], [$ztdPdo->beginTransaction(), $ztdPdo->inTransaction()]);
195    }
196
197    public function testCommitKeepsWhatTheTransactionWroteToTheShadow(): void
198    {
199        $native = new PDO('sqlite::memory:');
200        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
201        $native->exec("INSERT INTO users (name) VALUES ('ada'), ('grace')");
202        $ztdPdo = ZtdPdo::fromPdo($native);
203        $ztdPdo->beginTransaction();
204        $ztdPdo->exec("INSERT INTO users (id, name) VALUES (3, 'linus')");
205
206        $committed = $ztdPdo->commit();
207
208        $statement = $ztdPdo->query('SELECT * FROM users');
209        self::assertSame([true, [['id' => 3, 'name' => 'linus']]], [
210            $committed,
211            $statement === false ? [] : $statement->fetchAll(PDO::FETCH_ASSOC),
212        ]);
213    }
214
215    public function testRollBackTakesBackWhatTheTransactionWroteToTheShadow(): void
216    {
217        $native = new PDO('sqlite::memory:');
218        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
219        $native->exec("INSERT INTO users (name) VALUES ('ada'), ('grace')");
220        $ztdPdo = ZtdPdo::fromPdo($native);
221        $ztdPdo->beginTransaction();
222        $ztdPdo->exec("INSERT INTO users (id, name) VALUES (3, 'linus')");
223
224        $rolledBack = $ztdPdo->rollBack();
225
226        $statement = $ztdPdo->query('SELECT * FROM users');
227        self::assertSame([true, []], [
228            $rolledBack,
229            $statement === false ? [false] : $statement->fetchAll(PDO::FETCH_ASSOC),
230        ]);
231    }
232
233    public function testInTransactionSaysNothingIsOpenBeforeOneIsBegun(): void
234    {
235        $native = new PDO('sqlite::memory:');
236        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
237        $native->exec("INSERT INTO users (name) VALUES ('ada'), ('grace')");
238        $ztdPdo = ZtdPdo::fromPdo($native);
239
240        self::assertFalse($ztdPdo->inTransaction());
241    }
242
243    public function testLastInsertIdAnswersTheKeyTheShadowGaveTheRowItWrote(): void
244    {
245        $native = new PDO('sqlite::memory:');
246        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
247        $native->exec("INSERT INTO users (name) VALUES ('ada'), ('grace')");
248        $ztdPdo = ZtdPdo::fromPdo($native);
249        $ztdPdo->exec("INSERT INTO users (name) VALUES ('linus')");
250
251        self::assertSame('1', $ztdPdo->lastInsertId());
252    }
253
254    public function testErrorCodeAnswersWhatTheDriverSaysWentWrongLast(): void
255    {
256        $native = new PDO('sqlite::memory:');
257        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
258        $native->exec("INSERT INTO users (name) VALUES ('ada'), ('grace')");
259        $ztdPdo = ZtdPdo::fromPdo($native);
260
261        self::assertSame('00000', $ztdPdo->errorCode());
262    }
263
264    public function testErrorInfoAnswersWhatTheDriverSaysAboutTheLastFailure(): void
265    {
266        $native = new PDO('sqlite::memory:');
267        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
268        $native->exec("INSERT INTO users (name) VALUES ('ada'), ('grace')");
269        $ztdPdo = ZtdPdo::fromPdo($native);
270
271        self::assertSame('00000', $ztdPdo->errorInfo()[0]);
272    }
273
274    public function testGetAttributeReadsTheAttributeOffTheConnectionItWraps(): void
275    {
276        $native = new PDO('sqlite::memory:');
277        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
278        $native->exec("INSERT INTO users (name) VALUES ('ada'), ('grace')");
279        $ztdPdo = ZtdPdo::fromPdo($native);
280
281        self::assertSame('sqlite', $ztdPdo->getAttribute(PDO::ATTR_DRIVER_NAME));
282    }
283
284    public function testSetAttributeSetsTheAttributeOnTheConnectionItWraps(): void
285    {
286        $native = new PDO('sqlite::memory:');
287        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
288        $native->exec("INSERT INTO users (name) VALUES ('ada'), ('grace')");
289        $ztdPdo = ZtdPdo::fromPdo($native);
290
291        $ztdPdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_NUM);
292
293        self::assertSame(PDO::FETCH_NUM, $ztdPdo->getAttribute(PDO::ATTR_DEFAULT_FETCH_MODE));
294    }
295
296    public function testQuoteWritesAValueTheWayTheDriverWouldQuoteIt(): void
297    {
298        $native = new PDO('sqlite::memory:');
299        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
300        $native->exec("INSERT INTO users (name) VALUES ('ada'), ('grace')");
301        $ztdPdo = ZtdPdo::fromPdo($native);
302
303        self::assertSame("'ada'", $ztdPdo->quote('ada'));
304    }
305
306    public function testGetAvailableDriversAnswersTheDriversPdoWasBuiltWith(): void
307    {
308        self::assertContains('sqlite', ZtdPdo::getAvailableDrivers());
309    }
310
311    public function testFromPdoRetainsTheExistingConnectionAndItsOptions(): void
312    {
313        $native = new PDO('sqlite::memory:', options: [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);
314        $native->exec('CREATE TABLE items (id INTEGER PRIMARY KEY)');
315        $pdo = ZtdPdo::fromPdo($native);
316        self::assertSame(1, $pdo->exec('INSERT INTO items VALUES (7)'));
317        $result5 = $pdo->query('SELECT id FROM items');
318        self::assertNotFalse($result5);
319        self::assertSame(['id' => 7], $result5->fetch());
320        $result6 = $native->query('SELECT COUNT(*) FROM items');
321        self::assertNotFalse($result6);
322        self::assertSame(0, $result6->fetchColumn());
323    }
324
325    public function testPrepareWrapsUnsupportedStatementsForPdoConsumers(): void
326    {
327        $pdo = ZtdPdo::fromPdo(new PDO('sqlite::memory:'));
328        try {
329            $pdo->prepare('VACUUM');
330            self::fail('Unsupported maintenance statements must be rejected.');
331        } catch (ZtdPdoException $failure) {
332            self::assertStringContainsString('Statement type not supported', $failure->getMessage());
333            self::assertSame(0, $failure->getCode());
334            self::assertInstanceOf(\ZtdQuery\Connection\Exception\DatabaseException::class, $failure->getPrevious());
335        }
336    }
337}
338