packages/ztd-query-core/tests/Unit/QueryExecutorTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\UsesClass;
9use PHPUnit\Framework\TestCase;
10use Tests\Fake\FakeConnection;
11use Tests\Fake\FakeSqlRewriter;
12use Tests\Fake\FakeStatement;
13use ZtdQuery\Config\ZtdConfig;
14use ZtdQuery\Connection\Exception\DatabaseException;
15use ZtdQuery\Connection\ResultSet;
16use ZtdQuery\Exception\ForeignKeyViolationException;
17use ZtdQuery\Exception\MissingPrimaryKeyException;
18use ZtdQuery\Platform\CopySupport;
19use ZtdQuery\Platform\CopyTarget;
20use ZtdQuery\Platform\MissingResultColumnTypeResolver;
21use ZtdQuery\Platform\ParameterBindingCompiler;
22use ZtdQuery\Platform\ResultColumnTypeResolver;
23use ZtdQuery\QueryExecutor;
24use ZtdQuery\ResultSelectRunner;
25use ZtdQuery\Rewrite\QueryKind;
26use ZtdQuery\Rewrite\RewritePlan;
27use ZtdQuery\RewriteRefusal;
28use ZtdQuery\Schema\Key\CandidateKeySet;
29use ZtdQuery\Schema\Key\ForeignKeyDefinition;
30use ZtdQuery\Schema\TableDefinition;
31use ZtdQuery\Schema\TableDefinitionRegistry;
32use ZtdQuery\Shadow\Mutation\MutationRowIdentity;
33use ZtdQuery\Shadow\Mutation\Row\InsertMutation;
34use ZtdQuery\Shadow\Mutation\Row\UpdateMutation;
35use ZtdQuery\Shadow\ReferentialIntegrityEnforcer;
36use ZtdQuery\Shadow\Row\RowPairing;
37use ZtdQuery\Shadow\ShadowStore;
38use ZtdQuery\Shadow\ShadowTransactions;
39use ZtdQuery\Sql\TransactionStatement;
40
41#[CoversClass(QueryExecutor::class)]
42#[UsesClass(\ZtdQuery\Schema\ColumnDeclaration::class)]
43#[UsesClass(\ZtdQuery\Schema\Key\CandidateKeyMatch::class)]
44#[UsesClass(ZtdConfig::class)]
45#[UsesClass(\ZtdQuery\Session::class)]
46#[UsesClass(\ZtdQuery\Schema\ViewDefinitionSet::class)]
47#[UsesClass(ShadowStore::class)]
48#[UsesClass(ShadowTransactions::class)]
49#[UsesClass(TableDefinitionRegistry::class)]
50#[UsesClass(TableDefinition::class)]
51#[UsesClass(CandidateKeySet::class)]
52#[UsesClass(ResultSelectRunner::class)]
53#[UsesClass(ResultSet::class)]
54#[UsesClass(DatabaseException::class)]
55#[UsesClass(RewritePlan::class)]
56#[UsesClass(UpdateMutation::class)]
57#[UsesClass(InsertMutation::class)]
58#[UsesClass(MutationRowIdentity::class)]
59#[UsesClass(ForeignKeyDefinition::class)]
60#[UsesClass(ForeignKeyViolationException::class)]
61#[UsesClass(ReferentialIntegrityEnforcer::class)]
62#[UsesClass(MissingPrimaryKeyException::class)]
63#[UsesClass(CopyTarget::class)]
64#[UsesClass(MissingResultColumnTypeResolver::class)]
65#[UsesClass(TransactionStatement::class)]
66#[UsesClass(\ZtdQuery\Shadow\ShadowApplication::class)]
67#[UsesClass(\ZtdQuery\Shadow\Mutation\MutationImpact::class)]
68#[UsesClass(\ZtdQuery\GenericExecuteResult::class)]
69#[UsesClass(\ZtdQuery\Shadow\ShadowSavepoint::class)]
70#[UsesClass(\ZtdQuery\Shadow\Mutation\RowConstraints::class)]
71#[UsesClass(\ZtdQuery\Shadow\Mutation\ConflictSearch::class)]
72#[UsesClass(\ZtdQuery\Shadow\ForeignKeyCascade::class)]
73#[UsesClass(\ZtdQuery\Shadow\ForeignKeyEnds::class)]
74#[UsesClass(\ZtdQuery\Shadow\ForeignKeyIntegrity::class)]
75#[UsesClass(\ZtdQuery\Shadow\ParentKeyLookup::class)]
76#[UsesClass(\ZtdQuery\Shadow\Row\RowMatch::class)]
77#[UsesClass(\ZtdQuery\Shadow\Row\RowMultiset::class)]
78#[UsesClass(\ZtdQuery\Shadow\Row\TableTransition::class)]
79#[UsesClass(\ZtdQuery\Shadow\TableTransitions::class)]
80#[UsesClass(RewriteRefusal::class)]
81#[UsesClass(RowPairing::class)]
82#[UsesClass(\ZtdQuery\Schema\RowSet::class)]
83final class QueryExecutorTest extends TestCase
84{
85 public function testDisableEnableDisableEnableAndDisable(): void
86 {
87 $shadowStore = new ShadowStore();
88 $registry = new TableDefinitionRegistry();
89 $rewriter = new FakeSqlRewriter($shadowStore, $registry);
90 $connection = new FakeConnection();
91 $executor = \Tests\Fake\QueryExecutorBuilder::create(
92 $rewriter,
93 $shadowStore,
94 new ResultSelectRunner(),
95 ZtdConfig::default(),
96 $connection,
97 );
98
99 self::assertTrue($executor->session()->isEnabled());
100 self::assertNull($executor->session()->tableDefinition('users'));
101 self::assertNull($executor->platform()->copySupport());
102 self::assertNull($executor->copyTarget('users', null));
103 self::assertNull($executor->platform()->parameterBindingCompiler());
104 self::assertInstanceOf(MissingResultColumnTypeResolver::class, $executor->platform()->resultColumnTypeResolver());
105
106 $executor->session()->disable();
107 self::assertFalse($executor->session()->isEnabled());
108
109 $executor->session()->enable();
110 self::assertTrue($executor->session()->isEnabled());
111 }
112
113 public function testTableDefinitionReturnsRegisteredSchemaOrNull(): void
114 {
115 $shadowStore = new ShadowStore();
116 $registry = new TableDefinitionRegistry();
117 $definition = new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], ['id'], []);
118 $registry->register('users', $definition);
119 $executor = \Tests\Fake\QueryExecutorBuilder::create(
120 new FakeSqlRewriter($shadowStore, $registry),
121 $shadowStore,
122 new ResultSelectRunner(),
123 ZtdConfig::default(),
124 new FakeConnection(),
125 registry: $registry,
126 );
127
128 self::assertSame($definition, $executor->session()->tableDefinition('users'));
129 self::assertNull($executor->session()->tableDefinition('missing'));
130 }
131
132 public function testParameterBindingCompilerResultColumnTypeResolverParameterBindingCompilerDelegatesCopyTargetsToTheConfiguredPlatformSupport(): void
133 {
134 $shadowStore = new ShadowStore();
135 $registry = new TableDefinitionRegistry();
136 $definition = new TableDefinition(['id'], ['id' => 'INTEGER'], ['id'], ['id'], []);
137 $registry->register('users', $definition);
138 $target = new CopyTarget(['public', 'users'], ['id']);
139 $copy = self::createStub(CopySupport::class);
140 $copy->method('tableName')->willReturnMap([
141 ['public.users', 'users'],
142 ['missing', 'missing'],
143 ]);
144 $copy->method('target')->willReturn($target);
145 $compiler = self::createStub(ParameterBindingCompiler::class);
146 $typeResolver = self::createStub(ResultColumnTypeResolver::class);
147 $executor = \Tests\Fake\QueryExecutorBuilder::create(
148 new FakeSqlRewriter($shadowStore, $registry),
149 $shadowStore,
150 new ResultSelectRunner(),
151 ZtdConfig::default(),
152 new FakeConnection(),
153 registry: $registry,
154 copySupport: $copy,
155 parameterBindingCompiler: $compiler,
156 resultColumnTypeResolver: $typeResolver,
157 );
158
159 self::assertSame($copy, $executor->platform()->copySupport());
160 self::assertSame($target, $executor->copyTarget('public.users', 'id'));
161 self::assertNull($executor->copyTarget('missing', null));
162 self::assertSame($compiler, $executor->platform()->parameterBindingCompiler());
163 self::assertSame($typeResolver, $executor->platform()->resultColumnTypeResolver());
164 }
165
166 public function testSplitStatementsUsesPlatformRewriter(): void
167 {
168 $shadowStore = new ShadowStore();
169 $rewriter = new FakeSqlRewriter($shadowStore, new TableDefinitionRegistry());
170 $executor = \Tests\Fake\QueryExecutorBuilder::create(
171 $rewriter,
172 $shadowStore,
173 new ResultSelectRunner(),
174 ZtdConfig::default(),
175 new FakeConnection(),
176 );
177
178 self::assertSame(
179 ['SELECT 1', 'SELECT 2'],
180 $executor->splitStatements(' SELECT 1; SELECT 2 '),
181 );
182 }
183
184 public function testSessionTransactionsRestoreTheSameCatalogUsedByTheRewriter(): void
185 {
186 $shadowStore = new ShadowStore();
187 $registry = new TableDefinitionRegistry();
188 $definition = new TableDefinition(['id'], ['id' => 'INT'], ['id'], [], []);
189 $registry->register('users', $definition);
190 $executor = \Tests\Fake\QueryExecutorBuilder::create(
191 new FakeSqlRewriter($shadowStore, $registry),
192 $shadowStore,
193 new ResultSelectRunner(),
194 ZtdConfig::default(),
195 new FakeConnection(),
196 registry: $registry,
197 );
198
199 $executor->session()->transactions()->begin();
200 $registry->unregister('users');
201 $executor->session()->transactions()->rollBack();
202
203 self::assertSame($definition, $registry->get('users'));
204 }
205
206 public function testMutationFailureIsConvertedToDatabaseException(): void
207 {
208 $shadowStore = new ShadowStore();
209 $shadowStore->set('users', [['id' => 1, 'name' => 'Alice']]);
210 $registry = new TableDefinitionRegistry();
211 $executor = \Tests\Fake\QueryExecutorBuilder::create(
212 new FakeSqlRewriter($shadowStore, $registry),
213 $shadowStore,
214 new ResultSelectRunner(),
215 ZtdConfig::default(),
216 new FakeConnection(),
217 );
218 $plan = new RewritePlan(
219 "SELECT 1 AS id, 'Bob' AS name",
220 QueryKind::WRITE_SIMULATED,
221 new UpdateMutation('users', []),
222 );
223
224 try {
225 $executor->processExecutedStatement($plan, new FakeStatement([['id' => 1, 'name' => 'Bob']]));
226 self::fail('Expected a database exception.');
227 } catch (DatabaseException $exception) {
228 self::assertSame(0, $exception->getCode());
229 self::assertInstanceOf(MissingPrimaryKeyException::class, $exception->getPrevious());
230 }
231 }
232
233 public function testForeignKeyFailureRestoresShadowState(): void
234 {
235 $shadowStore = new ShadowStore();
236 $shadowStore->set('parents', []);
237 $shadowStore->set('children', []);
238 $registry = new TableDefinitionRegistry();
239 $registry->register('parents', new TableDefinition(['id'], ['id' => 'INT'], ['id'], ['id'], []));
240 $registry->register('children', new TableDefinition(
241 ['id', 'parent_id'],
242 ['id' => 'INT', 'parent_id' => 'INT'],
243 ['id'],
244 ['id'],
245 [],
246 foreignKeys: ['fk_parent' => new ForeignKeyDefinition(['parent_id'], 'parents', ['id'])],
247 ));
248 $executor = \Tests\Fake\QueryExecutorBuilder::create(
249 new FakeSqlRewriter($shadowStore, $registry),
250 $shadowStore,
251 new ResultSelectRunner(),
252 ZtdConfig::default(),
253 new FakeConnection(),
254 registry: $registry,
255 );
256 $plan = new RewritePlan(
257 'SELECT 1 AS id, 999 AS parent_id',
258 QueryKind::WRITE_SIMULATED,
259 new InsertMutation('children'),
260 );
261
262 try {
263 $executor->processExecutedStatement($plan, new FakeStatement([['id' => 1, 'parent_id' => 999]]));
264 self::fail('Expected a database exception.');
265 } catch (DatabaseException $exception) {
266 self::assertInstanceOf(ForeignKeyViolationException::class, $exception->getPrevious());
267 self::assertSame([], $shadowStore->get('children'));
268 }
269 }
270
271 public function testIsEnabledFollowsWhatWasAskedFor(): void
272 {
273 $store = new ShadowStore();
274 $registry = new TableDefinitionRegistry();
275 $executor = \Tests\Fake\QueryExecutorBuilder::create(
276 new FakeSqlRewriter($store, $registry),
277 $store,
278 new ResultSelectRunner(),
279 ZtdConfig::default(),
280 new FakeConnection(),
281 registry: $registry,
282 );
283
284 self::assertTrue($executor->session()->isEnabled());
285
286 $executor->session()->disable();
287
288 self::assertFalse($executor->session()->isEnabled());
289 }
290
291 public function testDisableStopsZtdWithoutTouchingTheShadow(): void
292 {
293 $store = new ShadowStore();
294 $store->set('users', [['id' => 1]]);
295 $registry = new TableDefinitionRegistry();
296 $executor = \Tests\Fake\QueryExecutorBuilder::create(
297 new FakeSqlRewriter($store, $registry),
298 $store,
299 new ResultSelectRunner(),
300 ZtdConfig::default(),
301 new FakeConnection(),
302 registry: $registry,
303 );
304
305 $executor->session()->disable();
306
307 self::assertSame([['id' => 1]], $store->get('users'));
308 }
309
310 public function testShouldExecuteIsFalseOnlyForAPlanNothingIsToBeRunFor(): void
311 {
312 $store = new ShadowStore();
313 $registry = new TableDefinitionRegistry();
314 $executor = \Tests\Fake\QueryExecutorBuilder::create(
315 new FakeSqlRewriter($store, $registry),
316 $store,
317 new ResultSelectRunner(),
318 ZtdConfig::default(),
319 new FakeConnection(),
320 registry: $registry,
321 );
322
323 self::assertTrue($executor->shouldExecute(new RewritePlan('SELECT 1', QueryKind::READ)));
324 self::assertFalse($executor->shouldExecute(new RewritePlan('SELECT 1', QueryKind::SKIPPED)));
325 }
326
327 public function testNeedsPostProcessingIsTrueForTheKindsThatChangeTheShadow(): void
328 {
329 $store = new ShadowStore();
330 $registry = new TableDefinitionRegistry();
331 $executor = \Tests\Fake\QueryExecutorBuilder::create(
332 new FakeSqlRewriter($store, $registry),
333 $store,
334 new ResultSelectRunner(),
335 ZtdConfig::default(),
336 new FakeConnection(),
337 registry: $registry,
338 );
339
340 self::assertTrue($executor->needsPostProcessing(new RewritePlan('x', QueryKind::WRITE_SIMULATED)));
341 self::assertTrue($executor->needsPostProcessing(new RewritePlan('x', QueryKind::DDL_SIMULATED)));
342 self::assertFalse($executor->needsPostProcessing(new RewritePlan('x', QueryKind::READ)));
343 }
344
345 public function testCreateEmptyWriteResultAnswersASimulatedWriteWithNothingToFetch(): void
346 {
347 $store = new ShadowStore();
348 $registry = new TableDefinitionRegistry();
349 $executor = \Tests\Fake\QueryExecutorBuilder::create(
350 new FakeSqlRewriter($store, $registry),
351 $store,
352 new ResultSelectRunner(),
353 ZtdConfig::default(),
354 new FakeConnection(),
355 registry: $registry,
356 );
357
358 $result = $executor->createEmptyWriteResult();
359
360 self::assertSame(QueryKind::WRITE_SIMULATED, $result->kind());
361 self::assertSame([], $result->fetchAll());
362 }
363
364 public function testLastInsertIdIsFalseUntilSomethingHasBeenInserted(): void
365 {
366 $store = new ShadowStore();
367 $registry = new TableDefinitionRegistry();
368 $executor = \Tests\Fake\QueryExecutorBuilder::create(
369 new FakeSqlRewriter($store, $registry),
370 $store,
371 new ResultSelectRunner(),
372 ZtdConfig::default(),
373 new FakeConnection(),
374 registry: $registry,
375 );
376
377 self::assertFalse($executor->session()->lastInsertId());
378 }
379
380 public function testTransactionStatementIsNothingForAStatementThatIsNotOne(): void
381 {
382 $store = new ShadowStore();
383 $registry = new TableDefinitionRegistry();
384 $executor = \Tests\Fake\QueryExecutorBuilder::create(
385 new FakeSqlRewriter($store, $registry),
386 $store,
387 new ResultSelectRunner(),
388 ZtdConfig::default(),
389 new FakeConnection(),
390 registry: $registry,
391 );
392
393 self::assertNull($executor->transactionStatement('SELECT 1'));
394 }
395
396 public function testCommitTransactionKeepsWhatTheTransactionDid(): void
397 {
398 $store = new ShadowStore();
399 $store->set('users', [['id' => 1]]);
400 $registry = new TableDefinitionRegistry();
401 $executor = \Tests\Fake\QueryExecutorBuilder::create(
402 new FakeSqlRewriter($store, $registry),
403 $store,
404 new ResultSelectRunner(),
405 ZtdConfig::default(),
406 new FakeConnection(),
407 registry: $registry,
408 );
409
410 $executor->session()->transactions()->begin();
411 $store->set('users', []);
412 $executor->session()->transactions()->commit();
413 $executor->session()->transactions()->rollBack();
414
415 self::assertSame([], $store->get('users'));
416 }
417
418 public function testApplyTransactionStatementDoesWhatTheStatementSays(): void
419 {
420 $store = new ShadowStore();
421 $store->set('users', [['id' => 1]]);
422 $registry = new TableDefinitionRegistry();
423 $executor = \Tests\Fake\QueryExecutorBuilder::create(
424 new FakeSqlRewriter($store, $registry),
425 $store,
426 new ResultSelectRunner(),
427 ZtdConfig::default(),
428 new FakeConnection(),
429 registry: $registry,
430 );
431
432 TransactionStatement::begin()->apply($executor->session()->transactions());
433 $store->set('users', []);
434 TransactionStatement::rollback()->apply($executor->session()->transactions());
435
436 self::assertSame([['id' => 1]], $store->get('users'));
437 }
438
439 public function testCopySupportIsNothingWhereTheDialectHasNoCopy(): void
440 {
441 $store = new ShadowStore();
442 $registry = new TableDefinitionRegistry();
443 $executor = \Tests\Fake\QueryExecutorBuilder::create(
444 new FakeSqlRewriter($store, $registry),
445 $store,
446 new ResultSelectRunner(),
447 ZtdConfig::default(),
448 new FakeConnection(),
449 registry: $registry,
450 );
451
452 self::assertNull($executor->platform()->copySupport());
453 }
454
455 public function testCopyTargetIsNothingWhereTheDialectHasNoCopy(): void
456 {
457 $store = new ShadowStore();
458 $registry = new TableDefinitionRegistry();
459 $executor = \Tests\Fake\QueryExecutorBuilder::create(
460 new FakeSqlRewriter($store, $registry),
461 $store,
462 new ResultSelectRunner(),
463 ZtdConfig::default(),
464 new FakeConnection(),
465 registry: $registry,
466 );
467
468 self::assertNull($executor->copyTarget('users', null));
469 }
470
471 public function testParameterBindingCompilerIsNothingWhereTheDriverBindsThemItself(): void
472 {
473 $store = new ShadowStore();
474 $registry = new TableDefinitionRegistry();
475 $executor = \Tests\Fake\QueryExecutorBuilder::create(
476 new FakeSqlRewriter($store, $registry),
477 $store,
478 new ResultSelectRunner(),
479 ZtdConfig::default(),
480 new FakeConnection(),
481 registry: $registry,
482 );
483
484 self::assertNull($executor->platform()->parameterBindingCompiler());
485 }
486
487 public function testRewriteAnswersThePlanTheRewriterGives(): void
488 {
489 $store = new ShadowStore();
490 $registry = new TableDefinitionRegistry();
491 $executor = \Tests\Fake\QueryExecutorBuilder::create(
492 new FakeSqlRewriter($store, $registry),
493 $store,
494 new ResultSelectRunner(),
495 ZtdConfig::default(),
496 new FakeConnection(),
497 registry: $registry,
498 );
499
500 $plan = $executor->rewrite('SELECT 1');
501
502 self::assertSame(QueryKind::READ, $plan->kind());
503 }
504
505 public function testProcessExecutedStatementReadsAReadStatementStraightBack(): void
506 {
507 $store = new ShadowStore();
508 $registry = new TableDefinitionRegistry();
509 $executor = \Tests\Fake\QueryExecutorBuilder::create(
510 new FakeSqlRewriter($store, $registry),
511 $store,
512 new ResultSelectRunner(),
513 ZtdConfig::default(),
514 new FakeConnection(),
515 registry: $registry,
516 );
517 $plan = new RewritePlan('SELECT 1', QueryKind::READ);
518
519 $result = $executor->processExecutedStatement($plan, new FakeStatement([['id' => 1]]));
520
521 self::assertSame([['id' => 1]], $result->fetchAll());
522 }
523
524 public function testApplyShadowWritesTheMutationAndAnswersWhatItCameTo(): void
525 {
526 $store = new ShadowStore();
527 $store->set('users', []);
528 $registry = new TableDefinitionRegistry();
529 $executor = \Tests\Fake\QueryExecutorBuilder::create(
530 new FakeSqlRewriter($store, $registry),
531 $store,
532 new ResultSelectRunner(),
533 ZtdConfig::default(),
534 new FakeConnection(),
535 registry: $registry,
536 );
537
538 $impact = $executor->applyShadow(new InsertMutation('users'), new ResultSet([['id' => 1]], []), 'INSERT');
539
540 self::assertTrue($impact->isInsertLike());
541 self::assertSame([['id' => 1]], $store->get('users'));
542 }
543
544 public function testExecStatementAnswersHowManyRowsAReadStatementCameTo(): void
545 {
546 $store = new ShadowStore();
547 $registry = new TableDefinitionRegistry();
548 $executor = \Tests\Fake\QueryExecutorBuilder::create(
549 new FakeSqlRewriter($store, $registry),
550 $store,
551 new ResultSelectRunner(),
552 ZtdConfig::default(),
553 new FakeConnection(),
554 registry: $registry,
555 );
556
557 self::assertSame(0, $executor->execStatement('SELECT 1'));
558 }
559
560 public function testRunResultSelectAndApplyShadowReadsBackWhatTheStatementWouldHaveWritten(): void
561 {
562 $store = new ShadowStore();
563 $store->set('users', []);
564 $registry = new TableDefinitionRegistry();
565 $executor = \Tests\Fake\QueryExecutorBuilder::create(
566 new FakeSqlRewriter($store, $registry),
567 $store,
568 new ResultSelectRunner(),
569 ZtdConfig::default(),
570 new FakeConnection(),
571 registry: $registry,
572 );
573 $plan = new RewritePlan('SELECT 1', QueryKind::WRITE_SIMULATED, new InsertMutation('users'));
574
575 $rows = $executor->runResultSelectAndApplyShadow(
576 $plan,
577 static fn (string $sql): FakeStatement => new FakeStatement([['id' => 1]]),
578 );
579
580 self::assertSame([['id' => 1]], $rows);
581 }
582
583 public function testEnableTurnsZtdBackOn(): void
584 {
585 $store = new ShadowStore();
586 $registry = new TableDefinitionRegistry();
587 $executor = \Tests\Fake\QueryExecutorBuilder::create(
588 new FakeSqlRewriter($store, $registry),
589 $store,
590 new ResultSelectRunner(),
591 ZtdConfig::default(),
592 new FakeConnection(),
593 registry: $registry,
594 );
595 $executor->session()->disable();
596
597 $executor->session()->enable();
598
599 self::assertTrue($executor->session()->isEnabled());
600 }
601
602 public function testRollBackTransactionPutsTheShadowBackToWhereItBegan(): void
603 {
604 $store = new ShadowStore();
605 $store->set('users', [['id' => 1]]);
606 $registry = new TableDefinitionRegistry();
607 $executor = \Tests\Fake\QueryExecutorBuilder::create(
608 new FakeSqlRewriter($store, $registry),
609 $store,
610 new ResultSelectRunner(),
611 ZtdConfig::default(),
612 new FakeConnection(),
613 registry: $registry,
614 );
615
616 $executor->session()->transactions()->begin();
617 $store->set('users', []);
618 $executor->session()->transactions()->rollBack();
619
620 self::assertSame([['id' => 1]], $store->get('users'));
621 }
622
623 public function testResultColumnTypeResolverAnswersTheOneTheSessionWasBuiltWith(): void
624 {
625 $store = new ShadowStore();
626 $registry = new TableDefinitionRegistry();
627 $executor = \Tests\Fake\QueryExecutorBuilder::create(
628 new FakeSqlRewriter($store, $registry),
629 $store,
630 new ResultSelectRunner(),
631 ZtdConfig::default(),
632 new FakeConnection(),
633 registry: $registry,
634 );
635
636 self::assertInstanceOf(MissingResultColumnTypeResolver::class, $executor->platform()->resultColumnTypeResolver());
637 }
638
639 public function testTransactionsAnswersWhatATransactionStatementIsAppliedTo(): void
640 {
641 $store = new ShadowStore();
642 $store->set('users', [['id' => 1]]);
643 $registry = new TableDefinitionRegistry();
644 $executor = \Tests\Fake\QueryExecutorBuilder::create(
645 new FakeSqlRewriter($store, $registry),
646 $store,
647 new ResultSelectRunner(),
648 ZtdConfig::default(),
649 new FakeConnection(),
650 registry: $registry,
651 );
652
653 TransactionStatement::begin()->apply($executor->session()->transactions());
654 $store->set('users', []);
655 TransactionStatement::rollback()->apply($executor->session()->transactions());
656
657 self::assertSame([['id' => 1]], $store->get('users'));
658 }
659
660 public function testPlatformAndSessionKeepReusedDialectStateIsolated(): void
661 {
662 $platform = new \Tests\Fake\FakePlatform(new \Tests\Fake\FakeSchemaReflector([
663 'users' => 'CREATE TABLE users (id INT NOT NULL PRIMARY KEY)',
664 ]));
665 $first = new QueryExecutor(new FakeConnection(defaultRows: [['id' => 1]]), $platform);
666 $second = new QueryExecutor(new FakeConnection(defaultRows: [['id' => 2]]), $platform);
667 self::assertSame($platform, $first->platform());
668 self::assertSame($platform, $second->platform());
669 self::assertNotSame($first->session(), $second->session());
670 self::assertNotSame($first->session()->registry(), $second->session()->registry());
671 $first->session()->beginTransaction();
672 $first->execStatement('INSERT INTO users (id) VALUES (1)');
673 $second->execStatement('INSERT INTO users (id) VALUES (2)');
674 $first->session()->rollBackTransaction();
675 self::assertSame([], $first->session()->store()->get('users'));
676 self::assertSame([['id' => 2]], $second->session()->store()->get('users'));
677 self::assertSame('2', $second->session()->lastInsertId());
678 }
679
680 public function testSessionAcceptsAnExplicitCatalogWithoutReflectingThePhysicalConnection(): void
681 {
682 $connection = self::createMock(\ZtdQuery\Connection\ConnectionInterface::class);
683 $connection->expects(self::never())->method('query');
684 $session = new \ZtdQuery\Session();
685 $session->registry()->register('users', new TableDefinition(['id'], ['id' => 'INT'], ['id'], ['id'], []));
686 $session->store()->set('users', [['id' => 7]]);
687 $executor = new QueryExecutor($connection, new \Tests\Fake\FakePlatform(), session: $session);
688 self::assertSame($session, $executor->session());
689 self::assertStringContainsString('7', $executor->rewrite('SELECT * FROM users')->sql());
690 }
691
692 public function testSessionReflectionFailurePreventsRewriterConstruction(): void
693 {
694 $platform = self::createMock(\ZtdQuery\Platform::class);
695 $platform->expects(self::once())->method('reflectSchema')->willThrowException(new DatabaseException('catalog unavailable'));
696 $platform->expects(self::never())->method('createRewriter');
697 $this->expectException(DatabaseException::class);
698 new QueryExecutor(new FakeConnection(), $platform);
699 }
700}
701