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