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

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit;
6
7use PDO;
8use PDOException;
9use PHPUnit\Framework\Attributes\CoversClass;
10use PHPUnit\Framework\TestCase;
11use ReflectionException;
12use ZtdQuery\Adapter\Pdo\Driver\PdoStatement;
13use ZtdQuery\Adapter\Pdo\Session\BufferedRow;
14use ZtdQuery\Adapter\Pdo\Session\ParameterBinder;
15use ZtdQuery\Adapter\Pdo\Session\ParameterKind;
16use ZtdQuery\Adapter\Pdo\Session\PreparedQuery;
17use ZtdQuery\Adapter\Pdo\ZtdPdo;
18use ZtdQuery\Adapter\Pdo\ZtdPdoException;
19use ZtdQuery\Adapter\Pdo\ZtdPdoStatement;
20use ZtdQuery\Config\ZtdConfig;
21use ZtdQuery\Connection\ConnectionInterface;
22use ZtdQuery\Connection\Exception\DatabaseException;
23use ZtdQuery\Exception\MissingPrimaryKeyException;
24use ZtdQuery\Platform\ResultColumnTypeResolver;
25use ZtdQuery\ResultSelectRunner;
26use ZtdQuery\Rewrite\QueryKind;
27use ZtdQuery\Rewrite\RewritePlan;
28use ZtdQuery\Rewrite\SqlRewriter;
29use ZtdQuery\Schema\ColumnType;
30use ZtdQuery\Schema\ColumnTypeFamily;
31use ZtdQuery\Session;
32use ZtdQuery\Shadow\Mutation\UpdateMutation;
33use ZtdQuery\Shadow\ShadowStore;
34
35#[CoversClass(ZtdPdoStatement::class)]
36#[\PHPUnit\Framework\Attributes\UsesClass(ZtdPdoException::class)]
37#[\PHPUnit\Framework\Attributes\UsesClass(ZtdPdo::class)]
38#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Adapter\Pdo\Driver\PdoConnection::class)]
39#[\PHPUnit\Framework\Attributes\UsesClass(PdoStatement::class)]
40#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Adapter\Pdo\Session\StatementExecution::class)]
41#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Adapter\Pdo\Session\Bindings::class)]
42#[CoversClass(BufferedRow::class)]
43#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Adapter\Pdo\Session\DriverSessionFactory::class)]
44#[\PHPUnit\Framework\Attributes\UsesClass(ParameterKind::class)]
45#[\PHPUnit\Framework\Attributes\UsesClass(ParameterBinder::class)]
46#[\PHPUnit\Framework\Attributes\UsesClass(PreparedQuery::class)]
47#[\PHPUnit\Framework\Attributes\Medium]
48#[\PHPUnit\Framework\Attributes\UsesClass(\ZtdQuery\Adapter\Pdo\Session\ConnectionExecution::class)]
49final class ZtdPdoStatementTest extends TestCase
50{
51    /**
52     * @throws ReflectionException
53     */
54    public function testBufferedObjectHydrationUsesDeclaredProperties(): void
55    {
56        $pdo = new PDO('sqlite::memory:');
57        $pdo->exec('CREATE TABLE errors (id INTEGER PRIMARY KEY, message TEXT)');
58        $statement = ZtdPdo::fromPdo($pdo)->prepare("INSERT INTO errors VALUES (1, 'row message') RETURNING message");
59        self::assertInstanceOf(ZtdPdoStatement::class, $statement);
60        self::assertTrue($statement->execute());
61        $object = $statement->fetchObject(PDOException::class, ['constructor message', 7]);
62        self::assertInstanceOf(PDOException::class, $object);
63        self::assertSame('row message', $object->getMessage());
64        self::assertSame(7, $object->getCode());
65    }
66
67    public function testPassthroughFetchAllForwardsConstructorArguments(): void
68    {
69        $pdo = ZtdPdo::fromPdo(new PDO('sqlite::memory:'));
70        $statement = $pdo->prepare("SELECT 'row message' AS message");
71        self::assertInstanceOf(ZtdPdoStatement::class, $statement);
72        self::assertTrue($statement->execute());
73        $objects = $statement->fetchAll(PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, PDOException::class, ['constructor message', 7]);
74        self::assertCount(1, $objects);
75        self::assertInstanceOf(PDOException::class, $objects[0]);
76        self::assertSame('row message', $objects[0]->getMessage());
77        self::assertSame(7, $objects[0]->getCode());
78    }
79
80    public function testExecuteDelegatesWhenNoPlan(): void
81    {
82        $pdo = new PDO('sqlite::memory:');
83        $pdo->exec('CREATE TABLE t (id INTEGER)');
84        $inner = $pdo->prepare('INSERT INTO t VALUES (1)');
85        self::assertNotFalse($inner);
86
87        $session = new Session(static::createStub(SqlRewriter::class), new ShadowStore(), new ResultSelectRunner(), ZtdConfig::default(), static::createStub(ConnectionInterface::class));
88
89        $stmt = new ZtdPdoStatement($inner, $session, null);
90        self::assertTrue($stmt->execute());
91    }
92
93    public function testExecuteReturnsFalseWhenShouldExecuteIsFalse(): void
94    {
95        $pdo = new PDO('sqlite::memory:');
96        $pdo->exec('CREATE TABLE t (id INTEGER)');
97        $inner = $pdo->prepare('SELECT * FROM t');
98        self::assertNotFalse($inner);
99
100        $plan = new RewritePlan('SELECT 1', QueryKind::SKIPPED);
101
102        $session = new Session(static::createStub(SqlRewriter::class), new ShadowStore(), new ResultSelectRunner(), ZtdConfig::default(), static::createStub(ConnectionInterface::class));
103        $stmt = new ZtdPdoStatement($inner, $session, $plan);
104        self::assertFalse($stmt->execute());
105    }
106
107    public function testExecuteDelegatesWhenNoPostProcessingNeeded(): void
108    {
109        $pdo = new PDO('sqlite::memory:');
110        $pdo->exec('CREATE TABLE t (id INTEGER)');
111        $inner = $pdo->prepare('SELECT * FROM t');
112        self::assertNotFalse($inner);
113
114        $plan = new RewritePlan('SELECT * FROM t', QueryKind::READ);
115
116        $session = new Session(static::createStub(SqlRewriter::class), new ShadowStore(), new ResultSelectRunner(), ZtdConfig::default(), static::createStub(ConnectionInterface::class));
117        $stmt = new ZtdPdoStatement($inner, $session, $plan);
118        self::assertTrue($stmt->execute());
119    }
120
121    public function testExecuteWithoutPostProcessingRunsNativeStatementOnce(): void
122    {
123        $pdo = new PDO('sqlite::memory:');
124        $pdo->exec('CREATE TABLE t (id INTEGER)');
125        $inner = $pdo->prepare('INSERT INTO t VALUES (1)');
126        self::assertNotFalse($inner);
127
128        $plan = new RewritePlan('INSERT INTO t VALUES (1)', QueryKind::READ);
129        $session = new Session(static::createStub(SqlRewriter::class), new ShadowStore(), new ResultSelectRunner(), ZtdConfig::default(), static::createStub(ConnectionInterface::class));
130        $stmt = new ZtdPdoStatement($inner, $session, $plan);
131
132        self::assertTrue($stmt->execute());
133        $count = $pdo->query('SELECT COUNT(*) FROM t');
134        self::assertNotFalse($count);
135        self::assertSame(1, $count->fetchColumn());
136    }
137
138    public function testBindValueSurvivesPreparedStatementRecompilation(): void
139    {
140        $pdo = new PDO('sqlite::memory:');
141        $stmt = ZtdPdo::fromPdo($pdo)->prepare('SELECT ? AS value');
142        self::assertInstanceOf(ZtdPdoStatement::class, $stmt);
143
144        self::assertTrue($stmt->bindValue(1, 42, PDO::PARAM_INT));
145        self::assertTrue($stmt->execute());
146        self::assertSame(42, $stmt->fetchColumn());
147    }
148
149    public function testBindParamSurvivesPreparedStatementRecompilation(): void
150    {
151        $pdo = new PDO('sqlite::memory:');
152        $stmt = ZtdPdo::fromPdo($pdo)->prepare('SELECT ? AS value');
153        self::assertInstanceOf(ZtdPdoStatement::class, $stmt);
154
155        $value = 41;
156
157        self::assertTrue($stmt->bindParam(1, $value, PDO::PARAM_INT));
158        $value = 43;
159        self::assertTrue($stmt->execute());
160        self::assertSame(43, $stmt->fetchColumn());
161    }
162
163    public function testExecuteWrapsSimulationFailureAsAdapterException(): void
164    {
165        $pdo = new PDO('sqlite::memory:');
166        $inner = $pdo->prepare("SELECT 1 AS id, 'Bob' AS name");
167        self::assertNotFalse($inner);
168
169        $shadowStore = new ShadowStore();
170        $shadowStore->set('users', [['id' => 1, 'name' => 'Alice']]);
171        $typeResolver = static::createStub(ResultColumnTypeResolver::class);
172        $typeResolver->method('resolve')->willReturn(new ColumnType(ColumnTypeFamily::INTEGER, 'INTEGER'));
173        $session = new Session(
174            static::createStub(SqlRewriter::class),
175            $shadowStore,
176            new ResultSelectRunner(),
177            ZtdConfig::default(),
178            static::createStub(ConnectionInterface::class),
179            resultColumnTypeResolver: $typeResolver,
180        );
181        $plan = new RewritePlan(
182            "SELECT 1 AS id, 'Bob' AS name",
183            QueryKind::WRITE_SIMULATED,
184            new UpdateMutation('users', []),
185        );
186        $stmt = new ZtdPdoStatement($inner, $session, $plan);
187
188        try {
189            $stmt->execute();
190            self::fail('Expected a ZTD PDO exception.');
191        } catch (ZtdPdoException $exception) {
192            self::assertSame(0, $exception->getCode());
193            $databaseException = $exception->getPrevious();
194            self::assertInstanceOf(DatabaseException::class, $databaseException);
195            self::assertInstanceOf(MissingPrimaryKeyException::class, $databaseException->getPrevious());
196        }
197    }
198
199    public function testBindValueDelegatesToInner(): void
200    {
201        $pdo = new PDO('sqlite::memory:');
202        $pdo->exec('CREATE TABLE t (id INTEGER, name TEXT)');
203        $inner = $pdo->prepare('INSERT INTO t VALUES (:id, :name)');
204        self::assertNotFalse($inner);
205
206        $session = new Session(static::createStub(SqlRewriter::class), new ShadowStore(), new ResultSelectRunner(), ZtdConfig::default(), static::createStub(ConnectionInterface::class));
207
208        $stmt = new ZtdPdoStatement($inner, $session, null);
209        self::assertTrue($stmt->bindValue(1, 'test'));
210    }
211
212    public function testRowCountDelegatesToInnerWhenNoResult(): void
213    {
214        $pdo = new PDO('sqlite::memory:');
215        $pdo->exec('CREATE TABLE t (id INTEGER)');
216        $pdo->exec('INSERT INTO t VALUES (1)');
217        $pdo->exec('INSERT INTO t VALUES (2)');
218
219        $inner = $pdo->prepare('SELECT * FROM t');
220        self::assertNotFalse($inner);
221        $inner->execute();
222
223        $session = new Session(static::createStub(SqlRewriter::class), new ShadowStore(), new ResultSelectRunner(), ZtdConfig::default(), static::createStub(ConnectionInterface::class));
224
225        $stmt = new ZtdPdoStatement($inner, $session, null);
226        self::assertSame(0, $stmt->rowCount());
227    }
228
229    public function testCloseCursorDelegatesToInner(): void
230    {
231        $pdo = new PDO('sqlite::memory:');
232        $pdo->exec('CREATE TABLE t (id INTEGER)');
233        $inner = $pdo->prepare('SELECT * FROM t');
234        self::assertNotFalse($inner);
235        $inner->execute();
236
237        $session = new Session(static::createStub(SqlRewriter::class), new ShadowStore(), new ResultSelectRunner(), ZtdConfig::default(), static::createStub(ConnectionInterface::class));
238
239        $stmt = new ZtdPdoStatement($inner, $session, null);
240        self::assertTrue($stmt->closeCursor());
241    }
242
243    public function testColumnCountDelegatesToInner(): void
244    {
245        $pdo = new PDO('sqlite::memory:');
246        $pdo->exec('CREATE TABLE t (id INTEGER, name TEXT, value REAL)');
247        $inner = $pdo->prepare('SELECT * FROM t');
248        self::assertNotFalse($inner);
249        $inner->execute();
250
251        $session = new Session(static::createStub(SqlRewriter::class), new ShadowStore(), new ResultSelectRunner(), ZtdConfig::default(), static::createStub(ConnectionInterface::class));
252
253        $stmt = new ZtdPdoStatement($inner, $session, null);
254        self::assertSame(3, $stmt->columnCount());
255    }
256    public function testBindColumnFillsTheVariableTheColumnIsReadInto(): void
257    {
258        $native = new PDO('sqlite::memory:');
259        $inner = $native->prepare('SELECT 7 AS id');
260        self::assertNotFalse($inner);
261        $session = (new \ZtdQuery\Platform\Sqlite\SqliteSessionFactory())->create(new \ZtdQuery\Adapter\Pdo\Driver\PdoConnection($native), ZtdConfig::default());
262        $statement = new ZtdPdoStatement($inner, $session, null);
263        $id = null;
264
265        $statement->bindColumn('id', $id);
266        $statement->execute();
267        $statement->fetch(PDO::FETCH_BOUND);
268
269        self::assertSame('7', $id);
270    }
271
272
273
274
275
276    public function testExecuteAndPostProcessLetsZtdReadWhatTheStatementAnswered(): void
277    {
278        $native = new PDO('sqlite::memory:');
279        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
280        $statement = ZtdPdo::fromPdo($native)->prepare('INSERT INTO users (name) VALUES (?) RETURNING id, name');
281        self::assertInstanceOf(ZtdPdoStatement::class, $statement);
282        $statement->execute(['linus']);
283
284        self::assertSame([['id' => 1, 'name' => 'linus']], $statement->fetchAll(PDO::FETCH_ASSOC));
285    }
286
287
288
289    public function testFetchAnswersOneBufferedRowAtATime(): void
290    {
291        $native = new PDO('sqlite::memory:');
292        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
293        $statement = ZtdPdo::fromPdo($native)->prepare('INSERT INTO users (name) VALUES (?) RETURNING id, name');
294        self::assertInstanceOf(ZtdPdoStatement::class, $statement);
295        $statement->execute(['linus']);
296
297        self::assertSame(['id' => 1, 'name' => 'linus'], $statement->fetch(PDO::FETCH_ASSOC));
298    }
299
300    public function testFetchAnswersFalseOnceTheBufferedRowsRunOut(): void
301    {
302        $native = new PDO('sqlite::memory:');
303        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
304        $statement = ZtdPdo::fromPdo($native)->prepare('INSERT INTO users (name) VALUES (?) RETURNING id, name');
305        self::assertInstanceOf(ZtdPdoStatement::class, $statement);
306        $statement->execute(['linus']);
307        $statement->fetch(PDO::FETCH_ASSOC);
308
309        self::assertFalse($statement->fetch(PDO::FETCH_ASSOC));
310    }
311
312    public function testFetchAllAnswersEveryBufferedRowInTheModeItIsAsked(): void
313    {
314        $native = new PDO('sqlite::memory:');
315        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
316        $statement = ZtdPdo::fromPdo($native)->prepare('INSERT INTO users (name) VALUES (?) RETURNING id, name');
317        self::assertInstanceOf(ZtdPdoStatement::class, $statement);
318        $statement->execute(['linus']);
319
320        self::assertSame([[1, 'linus']], $statement->fetchAll(PDO::FETCH_NUM));
321    }
322
323    public function testFetchAllAnswersOneColumnWhereOnlyOneIsAsked(): void
324    {
325        $native = new PDO('sqlite::memory:');
326        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
327        $statement = ZtdPdo::fromPdo($native)->prepare('INSERT INTO users (name) VALUES (?) RETURNING id, name');
328        self::assertInstanceOf(ZtdPdoStatement::class, $statement);
329        $statement->execute(['linus']);
330
331        self::assertSame(['linus'], $statement->fetchAll(PDO::FETCH_COLUMN, 1));
332    }
333
334    public function testFetchColumnAnswersTheColumnAskedForInTheBufferedRow(): void
335    {
336        $native = new PDO('sqlite::memory:');
337        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
338        $statement = ZtdPdo::fromPdo($native)->prepare('INSERT INTO users (name) VALUES (?) RETURNING id, name');
339        self::assertInstanceOf(ZtdPdoStatement::class, $statement);
340        $statement->execute(['linus']);
341
342        self::assertSame('linus', $statement->fetchColumn(1));
343    }
344
345    /**
346     * @throws ReflectionException
347     */
348    public function testFetchObjectBuildsAnObjectOutOfTheBufferedRow(): void
349    {
350        $native = new PDO('sqlite::memory:');
351        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
352        $statement = ZtdPdo::fromPdo($native)->prepare('INSERT INTO users (name) VALUES (?) RETURNING id, name');
353        self::assertInstanceOf(ZtdPdoStatement::class, $statement);
354        $statement->execute(['linus']);
355
356        $object = $statement->fetchObject();
357
358        self::assertSame(['id' => 1, 'name' => 'linus'], $object === false ? [] : get_object_vars($object));
359    }
360
361    /**
362     * @throws ReflectionException
363     */
364    public function testFetchObjectAnswersFalseWhereThereIsNoBufferedRowLeft(): void
365    {
366        $native = new PDO('sqlite::memory:');
367        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
368        $statement = ZtdPdo::fromPdo($native)->prepare('INSERT INTO users (name) VALUES (?) RETURNING id, name');
369        self::assertInstanceOf(ZtdPdoStatement::class, $statement);
370        $statement->execute(['linus']);
371        $statement->fetchObject();
372
373        self::assertFalse($statement->fetchObject());
374    }
375
376    public function testSetFetchModeIsRememberedAcrossAReprepare(): void
377    {
378        $native = new PDO('sqlite::memory:');
379        $statement = ZtdPdo::fromPdo($native)->prepare('SELECT ? AS value');
380        self::assertInstanceOf(ZtdPdoStatement::class, $statement);
381        $statement->setFetchMode(PDO::FETCH_NUM);
382
383        $statement->execute([42]);
384
385        self::assertSame([[42]], $statement->fetchAll());
386    }
387
388
389
390
391
392
393
394    public function testErrorCodeAnswersWhatTheDriverSaysWentWrongLast(): void
395    {
396        $native = new PDO('sqlite::memory:');
397        $inner = $native->prepare('SELECT 1 AS id');
398        self::assertNotFalse($inner);
399        $session = (new \ZtdQuery\Platform\Sqlite\SqliteSessionFactory())->create(new \ZtdQuery\Adapter\Pdo\Driver\PdoConnection($native), ZtdConfig::default());
400        $statement = new ZtdPdoStatement($inner, $session, null);
401        $statement->execute();
402        self::assertSame('00000', $statement->errorCode());
403    }
404
405    public function testErrorCodeAnswersNothingWhereTheDriverSaysNothing(): void
406    {
407        $native = new PDO('sqlite::memory:');
408        $inner = $native->prepare('SELECT 1 AS id');
409        self::assertNotFalse($inner);
410        $session = (new \ZtdQuery\Platform\Sqlite\SqliteSessionFactory())->create(new \ZtdQuery\Adapter\Pdo\Driver\PdoConnection($native), ZtdConfig::default());
411        $statement = new ZtdPdoStatement($inner, $session, null);
412        self::assertSame('', $statement->errorCode());
413    }
414
415    public function testErrorInfoAnswersWhatTheDriverSaysAboutTheLastFailure(): void
416    {
417        $native = new PDO('sqlite::memory:');
418        $inner = $native->prepare('SELECT 1 AS id');
419        self::assertNotFalse($inner);
420        $session = (new \ZtdQuery\Platform\Sqlite\SqliteSessionFactory())->create(new \ZtdQuery\Adapter\Pdo\Driver\PdoConnection($native), ZtdConfig::default());
421        $statement = new ZtdPdoStatement($inner, $session, null);
422        $statement->execute();
423        self::assertSame('00000', $statement->errorInfo()[0]);
424    }
425
426    public function testGetAttributeReadsTheAttributeOffTheStatementItWraps(): void
427    {
428        $native = new PDO('sqlite::memory:');
429        $inner = $native->prepare('SELECT 1 AS id');
430        self::assertNotFalse($inner);
431        $session = (new \ZtdQuery\Platform\Sqlite\SqliteSessionFactory())->create(new \ZtdQuery\Adapter\Pdo\Driver\PdoConnection($native), ZtdConfig::default());
432        $statement = new ZtdPdoStatement($inner, $session, null);
433        $statement->execute();
434        $this->expectException(PDOException::class);
435        $statement->getAttribute(PDO::ATTR_CURSOR);
436    }
437
438    public function testSetAttributeSetsTheAttributeOnTheStatementItWraps(): void
439    {
440        $native = new PDO('sqlite::memory:');
441        $inner = $native->prepare('SELECT 1 AS id');
442        self::assertNotFalse($inner);
443        $session = (new \ZtdQuery\Platform\Sqlite\SqliteSessionFactory())->create(new \ZtdQuery\Adapter\Pdo\Driver\PdoConnection($native), ZtdConfig::default());
444        $statement = new ZtdPdoStatement($inner, $session, null);
445        $statement->execute();
446        try {
447            $expected = $inner->setAttribute(PDO::ATTR_CURSOR, PDO::CURSOR_FWDONLY);
448        } catch (PDOException $failure) {
449            $this->expectExceptionObject($failure);
450            $statement->setAttribute(PDO::ATTR_CURSOR, PDO::CURSOR_FWDONLY);
451            return;
452        }
453        self::assertSame($expected, $statement->setAttribute(PDO::ATTR_CURSOR, PDO::CURSOR_FWDONLY));
454    }
455
456    public function testGetColumnMetaAnswersWhatTheDriverSaysAboutAColumn(): void
457    {
458        $native = new PDO('sqlite::memory:');
459        $inner = $native->prepare('SELECT 1 AS id');
460        self::assertNotFalse($inner);
461        $session = (new \ZtdQuery\Platform\Sqlite\SqliteSessionFactory())->create(new \ZtdQuery\Adapter\Pdo\Driver\PdoConnection($native), ZtdConfig::default());
462        $statement = new ZtdPdoStatement($inner, $session, null);
463        $statement->execute();
464        $meta = $statement->getColumnMeta(0);
465
466        self::assertSame('id', $meta === false ? null : $meta['name']);
467    }
468
469    public function testNextRowsetMovesTheStatementOnToTheDriversNextResult(): void
470    {
471        $native = new PDO('sqlite::memory:');
472        $inner = $native->prepare('SELECT 1 AS id');
473        self::assertNotFalse($inner);
474        $session = (new \ZtdQuery\Platform\Sqlite\SqliteSessionFactory())->create(new \ZtdQuery\Adapter\Pdo\Driver\PdoConnection($native), ZtdConfig::default());
475        $statement = new ZtdPdoStatement($inner, $session, null);
476        $statement->execute();
477        $this->expectException(PDOException::class);
478        $statement->nextRowset();
479    }
480
481    public function testDebugDumpParamsWritesTheDumpRatherThanAnsweringIt(): void
482    {
483        $native = new PDO('sqlite::memory:');
484        $inner = $native->prepare('SELECT 1 AS id');
485        self::assertNotFalse($inner);
486        $session = (new \ZtdQuery\Platform\Sqlite\SqliteSessionFactory())->create(new \ZtdQuery\Adapter\Pdo\Driver\PdoConnection($native), ZtdConfig::default());
487        $statement = new ZtdPdoStatement($inner, $session, null);
488
489        ob_start();
490        $dumped = $statement->debugDumpParams();
491        $output = ob_get_clean();
492
493        self::assertTrue($dumped);
494        self::assertIsString($output);
495        self::assertStringContainsString('SELECT 1 AS id', $output);
496        self::assertStringContainsString('Params:  0', $output);
497    }
498
499    public function testGetIteratorWalksTheRowsZtdBuffered(): void
500    {
501        $native = new PDO('sqlite::memory:');
502        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
503        $statement = ZtdPdo::fromPdo($native)->prepare('INSERT INTO users (name) VALUES (?) RETURNING id, name');
504        self::assertInstanceOf(ZtdPdoStatement::class, $statement);
505        $statement->execute(['linus']);
506
507        self::assertSame([['id' => 1, 0 => 1, 'name' => 'linus', 1 => 'linus']], iterator_to_array($statement->getIterator()));
508    }
509
510    public function testGetIteratorWalksTheDriversOwnCursorWhereNothingWasBuffered(): void
511    {
512        $native = new PDO('sqlite::memory:');
513        $inner = $native->prepare('SELECT 1 AS id');
514        self::assertNotFalse($inner);
515        $session = (new \ZtdQuery\Platform\Sqlite\SqliteSessionFactory())->create(new \ZtdQuery\Adapter\Pdo\Driver\PdoConnection($native), ZtdConfig::default());
516        $statement = new ZtdPdoStatement($inner, $session, null);
517        $statement->execute();
518
519        self::assertCount(1, iterator_to_array($statement->getIterator()));
520    }
521
522
523
524
525
526
527
528
529    public function testFetchAllAnswersFalseForAColumnTheBufferedRowDoesNotHold(): void
530    {
531        $native = new PDO('sqlite::memory:');
532        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
533        $statement = ZtdPdo::fromPdo($native)->prepare('INSERT INTO users (name) VALUES (?) RETURNING id, name');
534        self::assertInstanceOf(ZtdPdoStatement::class, $statement);
535        $statement->execute(['linus']);
536
537        self::assertSame([false], $statement->fetchAll(PDO::FETCH_COLUMN, 5));
538    }
539
540    public function testFetchAllAnswersEveryBufferedRowAndNotJustTheFirst(): void
541    {
542        $pdo = new PDO('sqlite::memory:', null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
543        $pdo->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
544        $ztdPdo = ZtdPdo::fromPdo($pdo);
545        $statement = $ztdPdo->prepare("INSERT INTO users (name) VALUES ('ada'), ('grace') RETURNING id, name");
546        self::assertInstanceOf(ZtdPdoStatement::class, $statement);
547        $statement->execute();
548
549        self::assertSame(
550            [['id' => 1, 'name' => 'ada'], ['id' => 2, 'name' => 'grace']],
551            $statement->fetchAll(PDO::FETCH_ASSOC),
552        );
553    }
554    /**
555     * @throws ReflectionException
556     */
557    public function testSimulatedWritesWithoutReturningHaveNoFetchableRows(): void
558    {
559        $pdo = new PDO('sqlite::memory:');
560        $pdo->exec('CREATE TABLE users (id INTEGER PRIMARY KEY)');
561        $statement = ZtdPdo::fromPdo($pdo)->prepare('INSERT INTO users VALUES (1)');
562        self::assertInstanceOf(ZtdPdoStatement::class, $statement);
563        self::assertTrue($statement->execute());
564        self::assertSame(1, $statement->rowCount());
565        self::assertFalse($statement->fetch());
566        self::assertSame([], $statement->fetchAll());
567        self::assertFalse($statement->fetchColumn());
568        self::assertFalse($statement->fetchObject());
569    }
570
571    public function testBufferedColumnFetchDefaultsToTheFirstColumnAndThenExhausts(): void
572    {
573        $native = new PDO('sqlite::memory:');
574        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
575        $statement = ZtdPdo::fromPdo($native)->prepare('INSERT INTO users (name) VALUES (?) RETURNING id, name');
576        self::assertInstanceOf(ZtdPdoStatement::class, $statement);
577        $statement->execute(['linus']);
578        self::assertSame(1, $statement->fetchColumn());
579        self::assertFalse($statement->fetchColumn());
580        $native = new PDO('sqlite::memory:');
581        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
582        $statement = ZtdPdo::fromPdo($native)->prepare('INSERT INTO users (name) VALUES (?) RETURNING id, name');
583        self::assertInstanceOf(ZtdPdoStatement::class, $statement);
584        $statement->execute(['linus']);
585        self::assertSame([1], $statement->fetchAll(PDO::FETCH_COLUMN));
586    }
587
588    public function testPassthroughFetchAllPreservesKeyPairKeys(): void
589    {
590        $native = new PDO('sqlite::memory:');
591        $inner = $native->prepare("SELECT 'alice' AS name, 7 AS score UNION ALL SELECT 'bob', 9");
592        self::assertNotFalse($inner);
593        $session = (new \ZtdQuery\Platform\Sqlite\SqliteSessionFactory())->create(new \ZtdQuery\Adapter\Pdo\Driver\PdoConnection($native), ZtdConfig::default());
594        $statement = new ZtdPdoStatement($inner, $session, null);
595        self::assertTrue($statement->execute());
596        self::assertSame(['alice' => 7, 'bob' => 9], $statement->fetchAll(PDO::FETCH_KEY_PAIR));
597    }
598
599    public function testPassthroughFetchAllForwardsColumnAndCallbackArguments(): void
600    {
601        $native = new PDO('sqlite::memory:');
602        $inner = $native->prepare("SELECT 1 AS id, 'alice' AS name");
603        self::assertNotFalse($inner);
604        $session = (new \ZtdQuery\Platform\Sqlite\SqliteSessionFactory())->create(new \ZtdQuery\Adapter\Pdo\Driver\PdoConnection($native), ZtdConfig::default());
605        $statement = new ZtdPdoStatement($inner, $session, null);
606        self::assertTrue($statement->execute());
607        self::assertSame(['alice'], $statement->fetchAll(PDO::FETCH_COLUMN, 1));
608        self::assertTrue($statement->execute());
609        self::assertSame(['1:alice'], $statement->fetchAll(PDO::FETCH_FUNC, static fn (int $id, string $name): string => $id . ':' . $name));
610    }
611
612
613
614
615
616    public function testBufferedColumnFetchAllReturnsEveryRow(): void
617    {
618        $native = new PDO('sqlite::memory:');
619        $native->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
620        $statement = ZtdPdo::fromPdo($native)->query("INSERT INTO users VALUES (1, 'Ada'), (2, 'Grace') RETURNING id, name");
621        self::assertInstanceOf(ZtdPdoStatement::class, $statement);
622        self::assertSame(['Ada', 'Grace'], $statement->fetchAll(PDO::FETCH_COLUMN, 1));
623        self::assertSame([], $statement->fetchAll(PDO::FETCH_COLUMN, 1));
624    }
625
626    /**
627     * @throws ReflectionException When object hydration fails.
628     */
629    public function testFetchObjectDelegatesWithoutABufferedResult(): void
630    {
631        $native = new PDO('sqlite::memory:');
632        $inner = $native->query("SELECT 7 AS id, 'Ada' AS name");
633        self::assertNotFalse($inner);
634        $session = (new \ZtdQuery\Platform\Sqlite\SqliteSessionFactory())->create(new \ZtdQuery\Adapter\Pdo\Driver\PdoConnection($native), ZtdConfig::default());
635        $statement = new ZtdPdoStatement($inner, $session, null);
636        $row = $statement->fetchObject();
637        self::assertIsObject($row);
638        self::assertSame(['id' => 7, 'name' => 'Ada'], get_object_vars($row));
639        self::assertFalse($statement->fetchObject());
640    }
641}
642